package org.jugile.util;

/*

Copyright (C) 2007-2011 Jukka Rahkonen  email: jukka.rahkonen@iki.fi

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

*/

import java.io.InputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;


/**
 * "Because that, when they knew God, they glorified him not as God, neither were thankful; 
 *  but became vain in their imaginations, and their foolish heart was darkened.
 *  Professing themselves to be wise, they became fools," (Romans 1:21-22)
 *
 * ==========
 * 
 * @author jukka.rahkonen@iki.fi
 */
public class Node extends Jugile implements Serializable {

	private static final long serialVersionUID = 1L;
	private Node parent;
	private List<Node> children;
	private String name;
	private Object value;
	private int linenum; // parser sets 
	
	public Node(String name) {
		this.name = name;
	}
	public Node(String name, Object value) {
		this.name = name;
		this.value = value;
	}

	public void setLinenum(int v) { linenum = v; }
	public int getLinenum() { return linenum; }
	
	// ----- cdata -----
	public boolean isCdata() { return "! CDATA".equals(name); }
	public Node addCdata(String data) { return add("! CDATA",data); }
	public Node setCdata(String data) {
		Iterator<Node> i = children().iterator();
		while (i.hasNext()) {
			i.next();
			i.remove();
		}
		return addCdata(data);
	}
	public boolean hasCdata() { 
		for (Node n : children()) if (n.isCdata()) return true;
		return false;
	}
	
	// ----- html -----
	public boolean isTextNode() { return empty(name); }
	public boolean hasTextNodes() {
		if (!hasChildren()) return false;
		for (Node ch : children()) 
			if (ch.isTextNode()) return true;
		return false;
	}
	
	// ----- json array -----
	public boolean isJsonArray() { return "jsonArray".equals(name); }
	public boolean isJsonArrayItem() { 
		if (parent != null) return parent.isJsonArray();
		return false;
	}
	public void jsonify() {
		if (children == null) return;
		for (Node n : children) n.jsonify();
		Map<String,List<Node>> m = new HashMap<String,List<Node>>();
		for (Node n : children) {
			List<Node> lst = m.get(n.name);
			if (lst == null) lst = new ArrayList<Node>();
			lst.add(n);
			m.put(n.name, lst);
		}
		List<Node> ch2 = new ArrayList<Node>();
		for (String key : m.keySet()) {
			List<Node> lst = m.get(key);
			if (lst.size() > 1) {
				Node arr = new Node("jsonArray");
				arr.attr("name", key);
				for (Node n : lst) {
					arr.add(n);
				}
				arr.parent = this;
				ch2.add(arr);
			} else {
				lst.get(0).parent = this;
				ch2.add(lst.get(0));
			}
		}
		children = ch2;
	}
	

	
	// ----- set & add ----- 
	public Node set(String key, Object value) {
		Node n = findNode(key);
		if (n != null) return n.value(value); 
		return add(key, value);	
	}
	public Node add(String key) { 
		chk(key, "key can't be null");
		return add(new Node(key, value));
	}
	public Node add(String key, Object value) {
		chk(key, "key can't be null");
		add(new Node(key,value));
		return this;
	}
	public Node add(Node n) {
		chk(n, "node can't be null");
		if (children == null) children = new ArrayList<Node>();
		n.parent = this;
		children.add(n);
		return n;
	}
	
	// ----- children & parent -----
	public Node up() { return parent; }
	public Node parent() { return parent; }	
	public List<Node> children() {
		if (children == null) children = new ArrayList<Node>();
		return children;
	}
	public boolean hasChildren() {
		if (children == null) return false;
		return children.size() > 0;
	}
	public void removeChildren() {
		if (children != null) children = null;
	}
	public Node root() {
		// TODO: loop check / limit
		if (parent == null) return this;
		return parent.root();
	}
	public int count() {
		int res = 1;
		for (Node n : children()) res += n.count();
		return res;
	}
	public boolean isRoot() {
		if (parent == null) return true;
		return false;
	}
	public void delete() {
		if (parent == null) return;
		parent.children.remove(this);
	}
	private boolean deleted = false;
	public void markDeleted() { deleted = true; }
	
	// ----- name & value -----
	public Object value() {
		StringBuffer buf = new StringBuffer();
		for (Node n : children()) {
			if (n.isCdata()) {
				buf.append(n.value());
			}
		}
		if (buf.length() > 0) return buf.toString();
		return value; 
	}
	public Object getValue() { return value; }
	public Node value(Object o) { value = o; return this;}
	public String name() { return name; }
	public void setName(String v) { name = v; }
	public String toString() {
		if (value == null) return null;
		if (value instanceof String) {
			String res = ((String)value).replaceAll("&amp;","&");
			res = res.replaceAll("&gt;",">");
			res = res.replaceAll("&lt;","<");
			return res;
		}
		return value == null ? null : value.toString();
	}
	public String str() { return value == null? null:value.toString(); }


	// ----- node & list getters -----
	public Object get(String key) {
		Node n = node(key);
		if (n == null) return null;
		return n.value();
	}
	public Node node(String key) {
		if (empty(key)) {
			//print("key empty");
			return null;
		}
		return node(key.split("\\."), 0);
	}
	public String str(String key) {
		Object o = get(key);
		return o == null? null: o.toString();
	}
	
	private Node node(String keys[], int idx) {
		//print("node: "+ keys + " "+ idx);
		Node n = findNode(keys[idx]);
		//print("node: " + n.name());
		if (n == null) {
			//print("n == null");
			return null;
		}
		if (idx == keys.length-1) {
			//print("idx == keys.len-1");
			return n;
		}
		//print("n.node"+keys + " " + idx+1);
		return n.node(keys, idx+1);
	}
	private Node findNode(String key) {
		//print("findNode: " + key);
		for (Node n : children()) {
			//print("  n: " + n.name());
			if (key.equals(n.name())) {
				//print("  found: " + key + " = " + n.name() + " res: " + n);
				return n;
			}
		}
		return null;
	}
	
	public List<Node> list(String key) { return findList(key, false, null); }
	public List<Node> list(String key, String filter) { return findList(key, false, filter); }
	public List<Node> listAll(String key) { return findList(key, true, null); }
	
	private List<Node> findList(String key, boolean deep, String filter) {
		chk(!empty(key), "empty key");
		List<Node> res = new ArrayList<Node>();
		findNodes(key, res, deep, filter);
		return res;		
	}
	private void findNodes(String key, List<Node> res, boolean deep, String filter) {
		for (Node n : children()) {
			if (key.equals(n.name())) {
				if (filter != null) {
					String p[] = filter.split("=");
					if (p.length == 2) {
						p[0] = p[0].trim();
						p[1] = p[1].trim();
						if (eq(n.attr(p[0]),p[1])) res.add(n);
					}
				} else {
					res.add(n);
				}
			}
			if (deep) n.findNodes(key, res, deep, filter);
		}
	}

	public boolean isEmpty() {
		return (!hasChildren() && value() == null);
	}
	
	// ----- attributes -----
	private Map<String,String> attributes = new HashMap<String,String>();
	public String attr(String name) { return attributes.get(name); }
	public Map<String,String> attrs() { return attributes; }
	public Set<String> keys() { return attributes.keySet(); }
	public void removeAttr(String key) { attributes.remove(key); }
	public Node attr(String name, Object value) {
		return setAttr(name, (value == null)?null:value.toString());
	}
	public boolean hasAttr(String name) {
		return attributes.containsKey(name);
	}
	public boolean hasAttrs() {
		return !attributes.isEmpty();
	}
	private Node setAttr(String name, String value) {
		if (name == null) return this; // null safe
		if (value == null) {
			attributes.remove(name);
			return this;
		}
		value = value.replaceAll("\"", "");
		attributes.put(name, value);
		return this;
	}

	// -------- simple validation ---------
	public void validateElements(String elementNames) {
		for (Node n : children()) 
			if (!contains(elementNames,n.name)) 
				fail("invalid element: " + n.name + " parent element: " + name + " line: " +linenum);
	}
	public void validateAttributes(String attrNames) {
		for (String a : attributes.keySet()) 
			if (!contains(attrNames,a)) 
				fail("invalid attribute: " + a + " line: " + linenum);
	}

	private boolean contains(String strings, String str) {
		for (String s : strings.split(" ")) if (s.equals(str)) return true;
		return false;
	}
	
	// -------- rendering ----------
	public static class RCtx {
		private Map<String,Object> ctx = new HashMap<String,Object>();
		public RCtx(Object ctrl) {
			ctx.put("c", ctrl);
		}
		public void put(String key, Object o) { ctx.put(key, o); }
		public Object eval(String key) {
			print("eval: " + key);
			Proxy p = new Proxy(ctx);
			return p.get(key);
		}
		
		private int textnodecount = 0;
		public String getTextNodePrefix() {
			if (textnodecount > 0) return "\n"+getIndent();
			textnodecount++;
			return "";
		}
		public void resetTextNodePrefix() {
			textnodecount = 0;
		}
		
		private int indent = 0;
		public void increaseIndent() { indent++; }
		public void decreaseIndent() {
			indent--;
			if (indent < 0) indent = 0;
		}
		public boolean hasIndent() { return indent > 0; }
		
		public String getIndent() {
			String res = "";
			for (int i = 0; i < indent; i++) res += "  ";
			return res;
		}
	}

	public void traverse(INodeTraverser listener) {
		listener.handle(this);
		if (deleted) return;
		List<Node> deletelist = new ArrayList<Node>();
		List<Node> list = children();
		int oldidx = listener.getIdx();
		int oldsize = listener.getSize();
		int size = list.size();
		for (int i = 0; i < list.size(); i++) {
			Node n = list.get(i);
			listener.setIdx(i,size);
			n.traverse(listener);
			if (n.deleted) deletelist.add(n);
		}
		for (Node n : deletelist) children.remove(n);
		listener.setIdx(oldidx, oldsize);
		listener.after(this);
	}

	abstract class BaseRenderer {
		protected RCtx rctx = new RCtx(null);
		protected StringBuffer buf = new StringBuffer();

		protected int idx;
		public int getIdx() { return idx; }
		protected int size;
		public int getSize() { return size; }
		public void setIdx(int i, int size) { idx = i; this.size = size; }
		protected boolean isLast() { return idx >= size-1; }
		protected boolean isFirst() { return idx == 0; }	
	}
	
	class Renderer extends BaseRenderer implements INodeTraverser {
		public void handle(Node n) {
			if (n.isTextNode()) {
				buf.append(xmlEncode(n.value()));
				return;
			}
			if (n.isCdata()) {
				buf.append("<![CDATA[");
				buf.append(nn(n.value));
				buf.append("]]>");
				return;
			}
			buf.append("\n" + rctx.getIndent() + "<" + n.name());
			if (n.hasAttrs()) {
				for (String key : n.keys()) {
					buf.append(" " + key + "=\"" + n.attr(key) + "\"");					
				}
			}
			if (n.isEmpty()) {
				buf.append("/>");
				return;
			}
			buf.append(">");
			
			if (n.value != null) buf.append(xmlEncode(n.value));
			if (n.hasChildren() && !n.hasTextNodes()) {
				rctx.increaseIndent();
			}
		}
		public void after(Node n) {
			if (n.isTextNode()) return;
			if (n.isEmpty()) return;
			if (n.isCdata()) return;
			if (n.hasChildren() && !n.hasTextNodes()) {
				rctx.decreaseIndent();
				if (!n.hasCdata()) buf.append("\n" + rctx.getIndent());
			}
			buf.append("</" + n.name() + "> ");
		}
		public String toString() { return buf.toString(); }
	}
	
	/**
	 * replace & < > chars.
	 */
	protected Object xmlEncode(Object o) {
		if (!(o instanceof String)) return o; 
		String str = (String)o;
		str = str.replaceAll("&", "&amp;");
		str = str.replaceAll("<", "&lt;");
		str = str.replaceAll(">", "&gt;");
		return str;
	}
	
	private String getText() {
		StringBuffer buf = new StringBuffer();
		if (value != null) buf.append(value.toString());
		for (Node n : children()) {
			if (n.isTextNode()) buf.append(" " + n.value);
		}
		return buf.toString();
	}
	private String getJsonText() {
		if (value == null) return "null";
		if (value instanceof Integer) return value.toString();
		if (value instanceof Double) return value.toString();
		if (value instanceof Float) return value.toString();
		if (value instanceof Boolean) return value.toString();
		return "\"" + replaceControls(value.toString()) + "\"";
	}
	
	class JsonRenderer extends BaseRenderer implements INodeTraverser {
		public void handle(Node n) {
			if (n.isTextNode()) return;
			if (n.isJsonArray()) {
				buf.append("\n" + rctx.getIndent() + "\"" + n.attr("name") +"\": ");
				buf.append("[");
				return;
			}
			String def = "\n" + rctx.getIndent() + "\"" + n.name() +"\": ";
			if ((n.hasChildren() || n.hasAttrs()) && !n.hasTextNodes()) {
				if (!n.isJsonArrayItem()) buf.append(def);
				buf.append("{");
			} else {
				if (!n.isJsonArrayItem()) buf.append(def);
				buf.append(n.getJsonText());
			}
			if (n.hasAttrs()) {
				rctx.increaseIndent();
				int count = 0;
				for (String key : n.keys()) {
					if (count++ > 0) buf.append(","); // TODO: clean this
					buf.append("\n" + rctx.getIndent() + "\"" + key + "\": \"" + n.attr(key) +"\"");
				}
				if (n.hasTextNodes() || n.hasChildren()) buf.append(","); // TODO: clean this
				else {
					buf.append("\n"+ rctx.getIndent() + "}");
				}
				rctx.decreaseIndent();
			}
			if (n.hasChildren() && !n.hasTextNodes()) {
				rctx.increaseIndent();
			}
		}
		public void after(Node n) {
			if (n.isTextNode()) return;
			if (n.isEmpty()) {
				// nothing
			} else if (n.isJsonArray()) {
				buf.append("]");
			} else if ((n.hasChildren() || n.hasAttrs()) && !n.hasTextNodes()) {
				rctx.decreaseIndent();
				buf.append("\n" + rctx.getIndent());
				buf.append("}");
			} else if (!n.hasChildren() && !n.hasTextNodes() && n.hasAttrs()){
				buf.append("###");
			}
			//buf.append(" ("+idx+"/"+size+")");
			if (!isLast()) buf.append(", ");
		}
		public String toString() { return buf.toString(); }
	}
	
	public String render() {
		Renderer r = new Renderer();
		traverse(r);
		return r.toString().trim() + "\n";
	}
	public String renderJson() {
		JsonRenderer r = new JsonRenderer();
		traverse(r);
		return r.toString().trim() + "\n";
	}
	

	// -------- eval ---------
	public String eval(Object ctrl) { return eval(new RCtx(ctrl)); }
	public String eval(RCtx rctx) {
		// ----- this node first -----
		if (isTextNode()) return rctx.getTextNodePrefix()+value();

		// commands 
		StringBuffer buf = new StringBuffer();
		StringBuffer attrs = new StringBuffer();
		if (hasAttrs()) {
			for (String key : keys()) {
				String val = attr(key);
				// ----- eval start -----
				if ("if".equals(key)) {
					Object test = rctx.eval(val);
					if (test instanceof Boolean) {
						if ((Boolean)test == false) return "";
					} else {
						if (test == null) return "";
					}
					continue;
				}
				if ("txt".equals(key)) {
					Object txt = rctx.eval(val);
					removeChildren();
					add("",txt);
					continue;
				}
				if ("for".equals(key)) {
					removeAttr(key);
					for (Object o : (List)rctx.eval(val)) {
						rctx.put("p", o);
						buf.append(eval(rctx));
					}
					return buf.toString();
				}
				// ----- eval end -----
				attrs.append(" " + key + "=\"" + val + "\"");					
			}
		}

		//  start tag
		buf.append("\n" + rctx.getIndent() + "<" + name());
		if (attrs.length() > 0) buf.append(attrs);
		if (isEmpty()) {
			buf.append("/>");
			return buf.toString();
		}
		buf.append(">");
		
		// ----- children -----
		for (Node ch : children()) {
			rctx.increaseIndent();
			buf.append(ch.eval(rctx));
			rctx.decreaseIndent();
		}
		
		if (!hasTextNodes()) buf.append("\n" + rctx.getIndent());
		rctx.resetTextNodePrefix();
		//  end tag
		buf.append("</" + name() + "> ");
		return buf.toString();
	}

	public static Node parse(String data) {
		fail("Node parser: not implemented");
		PullReader pr = new PullReader(data);
		return XmlParser.parse(pr);
	}
	
	public static Node parse(InputStream is) {
		fail("Node parser: not implemented");
		PullReader pr = new PullReader(is);
		return XmlParser.parse(pr);
	}
	
	public void write(String fname) {
		Blob b = new Blob();
		b.append(render());
		b.write(fname);
	}
	
	
}
